package lol.clann.minecraft.springboot.remap.remappers;

import lol.clann.minecraft.springboot.remap.ASMUtils;
import lol.clann.minecraft.springboot.remap.model.ClassMapping;
import relocate.org.objectweb.asm.commons.Remapper;
import relocate.org.objectweb.asm.commons.SignatureRemapper;
import relocate.org.objectweb.asm.signature.SignatureVisitor;

import java.util.Iterator;
import java.util.Map;
import java.util.Stack;

/**
 * 负责nsm->mcp的remap
 *
 * @author pyz
 * @date 2019/7/3 10:38 PM
 */
public class SBPJarRemapper extends Remapper {

    public final SBPJarMapping jarMapping;

    @Override
    public String mapSignature(String signature, boolean typeSignature) {
        if (ASMUtils.isValidSingnature(signature)) {
            return super.mapSignature(signature, typeSignature);
        } else {
            return signature;
        }
    }

    public SBPJarRemapper(SBPJarMapping jarMapping) {
        this.jarMapping = jarMapping;
    }

    @Override
    public String map(String typeName) {
        return mapTypeName(typeName, jarMapping.packages, jarMapping.byNMSSrcName, typeName);
    }

    public static String mapTypeName(String typeName, Map<String, String> packageMap, Map<String, ClassMapping> classMap, String defaultIfUnmapped) {
        String mapped = mapClassName(typeName, packageMap, classMap);
        return mapped != null ? mapped : defaultIfUnmapped;
    }

    /**
     * Helper method to map a class name by package (prefix) or class (exact)
     */
    private static String mapClassName(String className, Map<String, String> packageMap, Map<String, ClassMapping> classMap) {
        if (classMap != null && classMap.containsKey(className)) {
            ClassMapping mapping = classMap.get(className);
            return mapping.getMcpSrcName();
        }

        int index = className.lastIndexOf('$');
        if (index != -1) {
            String outer = className.substring(0, index);
            String mapped = mapClassName(outer, packageMap, classMap);
            if (mapped == null) return null;
            return mapped + className.substring(index);
        }

        if (packageMap != null) {
            Iterator<String> iter = packageMap.keySet().iterator();
            while (iter.hasNext()) {
                String oldPackage = iter.next();
                if (matchClassPackage(oldPackage, className)) {
                    String newPackage = packageMap.get(oldPackage);

                    return moveClassPackage(newPackage, getSimpleName(oldPackage, className));
                }
            }
        }

        return null;
    }

    private static boolean matchClassPackage(String packageName, String className) {
        if (packageName.equals(".")) {
            return isDefaultPackage(className);
        }

        return className.startsWith(packageName);
    }

    private static String moveClassPackage(String packageName, String classSimpleName) {
        if (packageName.equals(".")) {
            return classSimpleName;
        }

        return packageName + classSimpleName;
    }

    private static boolean isDefaultPackage(String className) {
        return className.indexOf('/') == -1;
    }

    private static String getSimpleName(String oldPackage, String className) {
        if (oldPackage.equals(".")) {
            return className;
        }

        return className.substring(oldPackage.length());
    }

    @Override
    public String mapMethodName(String owner, String name, String desc) {
        return mapMethodName(owner, name, desc, 0);
    }

    @Override
    public String mapFieldName(String owner, String name, String desc) {
        return mapFieldName(owner, name, desc, 0);
    }

    public String mapFieldName(String owner, String name, String desc, int access) {
        String mapped = jarMapping.tryClimb(jarMapping.fields, owner, name, access);
        return mapped == null ? name : mapped;
    }

    public String mapMethodName(String owner, String name, String desc, int access) {
        String mapped = jarMapping.tryClimb(jarMapping.methods, owner, name + " " + desc, access);
        return mapped == null ? name : mapped;
    }

    @Override
    protected SignatureVisitor createSignatureRemapper(SignatureVisitor v) {
        return new ProguardSignatureFixer(v, this);
    }

    /**
     * Proguard has a problem where it will sometimes incorrectly output a method signature.
     * It will put the fully qualified obf name for the inner instead of the inner name.
     * So here we try and detect and fix that.
     * Example:
     * Bad:  (TK;)Lzt<TK;TT;TR;>.zt$a;
     * Good: (TK;)Lzt<TK;TT;TR;>.a;
     */
    static class ProguardSignatureFixer extends SignatureRemapper {
        private Stack<String> classNames = new Stack<String>();

        ProguardSignatureFixer(SignatureVisitor sv, Remapper m) {
            super(sv, m);
        }

        @Override
        public void visitClassType(String name) {
            classNames.push(name);
            super.visitClassType(name);
        }

        @Override
        public void visitInnerClassType(String name) {
            String outerClassName = classNames.pop();

            if (name.startsWith(outerClassName + '$')) {
                name = name.substring(outerClassName.length() + 1);
            }

            String className = outerClassName + '$' + name;
            classNames.push(className);
            super.visitInnerClassType(name);
        }

        @Override
        public void visitEnd() {
            classNames.pop();
            super.visitEnd();
        }
    }

}
